import { ConnectionPool } from '@/core/connection-pool';
import { Producer } from '@/core/producer';
import { Consumer } from '@/core/consumer';
import { Message } from '@/core/message';
import { MessageHandler, ExchangeConfig, ExchangeType, Logger } from '@/types';
import { createLogger } from '@/utils/logger';
import { generateUUID } from '@/utils/uuid';

/**
 * Publish/Subscribe pattern implementation
 * Broadcasts messages to all subscribers
 */
export class PubSub {
  private readonly connectionPool: ConnectionPool;
  private readonly exchangeName: string;
  private readonly exchangeConfig: ExchangeConfig;
  private readonly producer: Producer;
  private readonly logger: Logger;
  private readonly subscribers: Consumer[] = [];

  constructor(
    connectionPool: ConnectionPool,
    exchangeName: string,
    exchangeConfig: Partial<ExchangeConfig> = {},
    logger?: Logger
  ) {
    this.connectionPool = connectionPool;
    this.exchangeName = exchangeName;
    this.exchangeConfig = {
      name: exchangeName,
      type: ExchangeType.FANOUT,
      durable: true,
      autoDelete: false,
      ...exchangeConfig,
    };
    this.producer = new Producer(connectionPool);
    this.logger = logger ?? createLogger('PubSub');
  }

  /**
   * Setup the exchange
   */
  async setupExchange(): Promise<void> {
    try {
      const connection = await this.connectionPool.getConnection();
      const channel = connection.channel;

      await channel.assertExchange(this.exchangeConfig.name, this.exchangeConfig.type, {
        durable: this.exchangeConfig.durable,
        autoDelete: this.exchangeConfig.autoDelete,
        arguments: this.exchangeConfig.arguments,
      });

      this.logger.info(`PubSub exchange ${this.exchangeName} setup completed`);
    } catch (error) {
      this.logger.error(`Failed to setup PubSub exchange ${this.exchangeName}`, error as Error);
      throw error;
    }
  }

  /**
   * Publish a message to all subscribers
   */
  async publish(messageData: unknown, routingKey = ''): Promise<void> {
    const message = new Message(messageData);

    await this.producer.send(message, {
      exchange: this.exchangeName,
      routingKey,
      persistent: true,
    });

    this.logger.debug(`Message published to exchange ${this.exchangeName}`, {
      messageId: message.id,
      routingKey,
    });
  }

  /**
   * Publish multiple messages in batch
   */
  async publishBatch(messages: Array<{ data: unknown; routingKey?: string }>): Promise<void> {
    const messageObjects = messages.map(msg => new Message(msg.data));

    const results = await this.producer.sendBatch(messageObjects, {
      exchange: this.exchangeName,
      persistent: true,
    });

    const successCount = results.filter(r => r).length;
    this.logger.info(
      `Published ${successCount}/${messages.length} messages to exchange ${this.exchangeName}`
    );
  }

  /**
   * Subscribe to messages
   */
  async subscribe(
    handler: MessageHandler,
    options: {
      queueName?: string;
      exclusive?: boolean;
      autoDelete?: boolean;
      autoAck?: boolean;
      routingKey?: string;
    } = {}
  ): Promise<string> {
    const queueName = options.queueName || `${this.exchangeName}_${generateUUID()}`;

    try {
      const connection = await this.connectionPool.getConnection();
      const channel = connection.channel;

      // Declare queue
      await channel.assertQueue(queueName, {
        exclusive: options.exclusive ?? false,
        autoDelete: options.autoDelete ?? true,
        durable: false,
      });

      // Bind queue to exchange
      await channel.bindQueue(queueName, this.exchangeName, options.routingKey || '');

      // Create consumer
      const consumer = new Consumer(this.connectionPool, handler);
      await consumer.startConsuming({
        queueName,
        autoAck: options.autoAck ?? false,
        exclusive: options.exclusive ?? true,
      });

      this.subscribers.push(consumer);
      this.logger.info(`Subscribed to exchange ${this.exchangeName} with queue ${queueName}`);

      return queueName;
    } catch (error) {
      this.logger.error(`Failed to subscribe to exchange ${this.exchangeName}`, error as Error);
      throw error;
    }
  }

  /**
   * Unsubscribe from messages
   */
  async unsubscribe(queueName: string): Promise<void> {
    try {
      const connection = await this.connectionPool.getConnection();
      const channel = connection.channel;

      // Find and stop the consumer
      const consumerIndex = this.subscribers.findIndex(consumer =>
        consumer.getStatus().consumerTag?.includes(queueName)
      );

      if (consumerIndex !== -1) {
        await this.subscribers[consumerIndex].stopConsuming();
        this.subscribers.splice(consumerIndex, 1);
      }

      // Unbind and delete queue
      await channel.unbindQueue(queueName, this.exchangeName, '');
      await channel.deleteQueue(queueName);

      this.logger.info(
        `Unsubscribed from exchange ${this.exchangeName}, queue ${queueName} deleted`
      );
    } catch (error) {
      this.logger.error(`Failed to unsubscribe from queue ${queueName}`, error as Error);
      throw error;
    }
  }

  /**
   * Stop all subscribers
   */
  async stopAllSubscribers(): Promise<void> {
    await Promise.all(this.subscribers.map(subscriber => subscriber.stopConsuming()));
    this.subscribers.length = 0;
    this.logger.info(`Stopped all subscribers for exchange ${this.exchangeName}`);
  }

  /**
   * Get exchange information
   */
  async getExchangeInfo(): Promise<{ name: string; type: string; durable: boolean }> {
    try {
      const connection = await this.connectionPool.getConnection();
      const channel = connection.channel;

      // Note: RabbitMQ doesn't provide a direct way to get exchange info
      // This is a placeholder implementation
      return {
        name: this.exchangeConfig.name,
        type: this.exchangeConfig.type,
        durable: this.exchangeConfig.durable,
      };
    } catch (error) {
      this.logger.error(`Failed to get exchange info for ${this.exchangeName}`, error as Error);
      throw error;
    }
  }

  /**
   * Delete the exchange
   */
  async deleteExchange(options: { ifUnused?: boolean } = {}): Promise<void> {
    try {
      const connection = await this.connectionPool.getConnection();
      const channel = connection.channel;

      await channel.deleteExchange(this.exchangeName, options);
      this.logger.info(`Deleted exchange ${this.exchangeName}`);
    } catch (error) {
      this.logger.error(`Failed to delete exchange ${this.exchangeName}`, error as Error);
      throw error;
    }
  }

  /**
   * Close the PubSub
   */
  async close(): Promise<void> {
    await this.stopAllSubscribers();
    await this.producer.close();
    this.logger.info(`PubSub ${this.exchangeName} closed`);
  }
}

/**
 * Topic-based Publish/Subscribe pattern
 * Allows routing based on topic patterns
 */
export class TopicPubSub extends PubSub {
  constructor(
    connectionPool: ConnectionPool,
    exchangeName: string,
    exchangeConfig: Partial<ExchangeConfig> = {},
    logger?: Logger
  ) {
    const topicExchangeConfig = {
      ...exchangeConfig,
      type: ExchangeType.TOPIC,
    };

    super(connectionPool, exchangeName, topicExchangeConfig, logger);
  }

  /**
   * Publish to a specific topic
   */
  async publishToTopic(topic: string, messageData: unknown): Promise<void> {
    await this.publish(messageData, topic);
  }

  /**
   * Subscribe to a topic pattern
   */
  async subscribeToTopic(
    topicPattern: string,
    handler: MessageHandler,
    options: {
      queueName?: string;
      exclusive?: boolean;
      autoDelete?: boolean;
      autoAck?: boolean;
    } = {}
  ): Promise<string> {
    return await this.subscribe(handler, {
      ...options,
      routingKey: topicPattern,
    });
  }
}

/**
 * Direct Publish/Subscribe pattern
 * Routes messages based on exact routing key matches
 */
export class DirectPubSub extends PubSub {
  constructor(
    connectionPool: ConnectionPool,
    exchangeName: string,
    exchangeConfig: Partial<ExchangeConfig> = {},
    logger?: Logger
  ) {
    const directExchangeConfig = {
      ...exchangeConfig,
      type: ExchangeType.DIRECT,
    };

    super(connectionPool, exchangeName, directExchangeConfig, logger);
  }

  /**
   * Publish to a specific routing key
   */
  async publishToRoute(routingKey: string, messageData: unknown): Promise<void> {
    await this.publish(messageData, routingKey);
  }

  /**
   * Subscribe to a specific routing key
   */
  async subscribeToRoute(
    routingKey: string,
    handler: MessageHandler,
    options: {
      queueName?: string;
      exclusive?: boolean;
      autoDelete?: boolean;
      autoAck?: boolean;
    } = {}
  ): Promise<string> {
    return await this.subscribe(handler, {
      ...options,
      routingKey,
    });
  }
}

/**
 * Utility functions for creating PubSub instances
 */
export const createPubSub = (
  connectionPool: ConnectionPool,
  exchangeName: string,
  exchangeConfig?: Partial<ExchangeConfig>,
  logger?: Logger
): PubSub => {
  return new PubSub(connectionPool, exchangeName, exchangeConfig, logger);
};

export const createTopicPubSub = (
  connectionPool: ConnectionPool,
  exchangeName: string,
  exchangeConfig?: Partial<ExchangeConfig>,
  logger?: Logger
): TopicPubSub => {
  return new TopicPubSub(connectionPool, exchangeName, exchangeConfig, logger);
};

export const createDirectPubSub = (
  connectionPool: ConnectionPool,
  exchangeName: string,
  exchangeConfig?: Partial<ExchangeConfig>,
  logger?: Logger
): DirectPubSub => {
  return new DirectPubSub(connectionPool, exchangeName, exchangeConfig, logger);
};
